import { Reporter as Reporter$0 } from "../base/reporter";
import { EncoderBuffer as EncoderBuffer$0 } from "../base/buffer";
import { DecoderBuffer as DecoderBuffer$0 } from "../base/buffer";
import assert from "assert";
'use strict';
const Reporter = { Reporter: Reporter$0 }.Reporter;
const EncoderBuffer = { EncoderBuffer: EncoderBuffer$0 }.EncoderBuffer;
const DecoderBuffer = { DecoderBuffer: DecoderBuffer$0 }.DecoderBuffer;
// Supported tags
const tags = [
    'seq', 'seqof', 'set', 'setof', 'objid', 'bool',
    'gentime', 'utctime', 'null_', 'enum', 'int', 'objDesc',
    'bitstr', 'bmpstr', 'charstr', 'genstr', 'graphstr', 'ia5str', 'iso646str',
    'numstr', 'octstr', 'printstr', 't61str', 'unistr', 'utf8str', 'videostr'
];
// Public methods list
const methods = [
    'key', 'obj', 'use', 'optional', 'explicit', 'implicit', 'def', 'choice',
    'any', 'contains'
].concat(tags);
// Overrided methods list
const overrided = [
    '_peekTag', '_decodeTag', '_use',
    '_decodeStr', '_decodeObjid', '_decodeTime',
    '_decodeNull', '_decodeInt', '_decodeBool', '_decodeList',
    '_encodeComposite', '_encodeStr', '_encodeObjid', '_encodeTime',
    '_encodeNull', '_encodeInt', '_encodeBool'
];
function Node(enc, parent, name) {
    const state = {};
    this._baseState = state;
    state.name = name;
    state.enc = enc;
    state.parent = parent || null;
    state.children = null;
    // State
    state.tag = null;
    state.args = null;
    state.reverseArgs = null;
    state.choice = null;
    state.optional = false;
    state.any = false;
    state.obj = false;
    state.use = null;
    state.useDecoder = null;
    state.key = null;
    state['default'] = null;
    state.explicit = null;
    state.implicit = null;
    state.contains = null;
    // Should create new instance on each method
    if (!state.parent) {
        state.children = [];
        this._wrap();
    }
}
const stateProps = [
    'enc', 'parent', 'children', 'tag', 'args', 'reverseArgs', 'choice',
    'optional', 'any', 'obj', 'use', 'alteredUse', 'key', 'default', 'explicit',
    'implicit', 'contains'
];
Node.prototype.clone = function clone() {
    const state = this._baseState;
    const cstate = {};
    stateProps.forEach(function (prop) {
        cstate[prop] = state[prop];
    });
    const res = new this.constructor(cstate.parent);
    res._baseState = cstate;
    return res;
};
Node.prototype._wrap = function wrap() {
    const state = this._baseState;
    methods.forEach(function (method) {
        this[method] = function _wrappedMethod() {
            const clone = new this.constructor(this);
            state.children.push(clone);
            return clone[method].apply(clone, arguments);
        };
    }, this);
};
Node.prototype._init = function init(body) {
    const state = this._baseState;
    assert(state.parent === null);
    body.call(this);
    // Filter children
    state.children = state.children.filter(function (child) {
        return child._baseState.parent === this;
    }, this);
    assert.equal(state.children.length, 1, 'Root node can have only one child');
};
Node.prototype._useArgs = function useArgs(args) {
    const state = this._baseState;
    // Filter children and args
    const children = args.filter(function (arg) {
        return arg instanceof this.constructor;
    }, this);
    args = args.filter(function (arg) {
        return !(arg instanceof this.constructor);
    }, this);
    if (children.length !== 0) {
        assert(state.children === null);
        state.children = children;
        // Replace parent to maintain backward link
        children.forEach(function (child) {
            child._baseState.parent = this;
        }, this);
    }
    if (args.length !== 0) {
        assert(state.args === null);
        state.args = args;
        state.reverseArgs = args.map(function (arg) {
            if (typeof arg !== 'object' || arg.constructor !== Object)
                return arg;
            const res = {};
            Object.keys(arg).forEach(function (key) {
                if (key == (key | 0))
                    key |= 0;
                const value = arg[key];
                res[value] = key;
            });
            return res;
        });
    }
};
//
// Overrided methods
//
overrided.forEach(function (method) {
    Node.prototype[method] = function _overrided() {
        const state = this._baseState;
        throw new Error(method + ' not implemented for encoding: ' + state.enc);
    };
});
//
// Public methods
//
tags.forEach(function (tag) {
    Node.prototype[tag] = function _tagMethod() {
        const state = this._baseState;
        const args = Array.prototype.slice.call(arguments);
        assert(state.tag === null);
        state.tag = tag;
        this._useArgs(args);
        return this;
    };
});
Node.prototype.use = function use(item) {
    assert(item);
    const state = this._baseState;
    assert(state.use === null);
    state.use = item;
    return this;
};
Node.prototype.optional = function optional() {
    const state = this._baseState;
    state.optional = true;
    return this;
};
Node.prototype.def = function def(val) {
    const state = this._baseState;
    assert(state['default'] === null);
    state['default'] = val;
    state.optional = true;
    return this;
};
Node.prototype.explicit = function explicit(num) {
    const state = this._baseState;
    assert(state.explicit === null && state.implicit === null);
    state.explicit = num;
    return this;
};
Node.prototype.implicit = function implicit(num) {
    const state = this._baseState;
    assert(state.explicit === null && state.implicit === null);
    state.implicit = num;
    return this;
};
Node.prototype.obj = function obj() {
    const state = this._baseState;
    const args = Array.prototype.slice.call(arguments);
    state.obj = true;
    if (args.length !== 0)
        this._useArgs(args);
    return this;
};
Node.prototype.key = function key(newKey) {
    const state = this._baseState;
    assert(state.key === null);
    state.key = newKey;
    return this;
};
Node.prototype.any = function any() {
    const state = this._baseState;
    state.any = true;
    return this;
};
Node.prototype.choice = function choice(obj) {
    const state = this._baseState;
    assert(state.choice === null);
    state.choice = obj;
    this._useArgs(Object.keys(obj).map(function (key) {
        return obj[key];
    }));
    return this;
};
Node.prototype.contains = function contains(item) {
    const state = this._baseState;
    assert(state.use === null);
    state.contains = item;
    return this;
};
//
// Decoding
//
Node.prototype._decode = function decode(input, options) {
    const state = this._baseState;
    // Decode root node
    if (state.parent === null)
        return input.wrapResult(state.children[0]._decode(input, options));
    let result = state['default'];
    let present = true;
    let prevKey = null;
    if (state.key !== null)
        prevKey = input.enterKey(state.key);
    // Check if tag is there
    if (state.optional) {
        let tag = null;
        if (state.explicit !== null)
            tag = state.explicit;
        else if (state.implicit !== null)
            tag = state.implicit;
        else if (state.tag !== null)
            tag = state.tag;
        if (tag === null && !state.any) {
            // Trial and Error
            const save = input.save();
            try {
                if (state.choice === null)
                    this._decodeGeneric(state.tag, input, options);
                else
                    this._decodeChoice(input, options);
                present = true;
            }
            catch (e) {
                present = false;
            }
            input.restore(save);
        }
        else {
            present = this._peekTag(input, tag, state.any);
            if (input.isError(present))
                return present;
        }
    }
    // Push object on stack
    let prevObj;
    if (state.obj && present)
        prevObj = input.enterObject();
    if (present) {
        // Unwrap explicit values
        if (state.explicit !== null) {
            const explicit = this._decodeTag(input, state.explicit);
            if (input.isError(explicit))
                return explicit;
            input = explicit;
        }
        const start = input.offset;
        // Unwrap implicit and normal values
        if (state.use === null && state.choice === null) {
            let save;
            if (state.any)
                save = input.save();
            const body = this._decodeTag(input, state.implicit !== null ? state.implicit : state.tag, state.any);
            if (input.isError(body))
                return body;
            if (state.any)
                result = input.raw(save);
            else
                input = body;
        }
        if (options && options.track && state.tag !== null)
            options.track(input.path(), start, input.length, 'tagged');
        if (options && options.track && state.tag !== null)
            options.track(input.path(), input.offset, input.length, 'content');
        // Select proper method for tag
        if (state.any) {
            // no-op
        }
        else if (state.choice === null) {
            result = this._decodeGeneric(state.tag, input, options);
        }
        else {
            result = this._decodeChoice(input, options);
        }
        if (input.isError(result))
            return result;
        // Decode children
        if (!state.any && state.choice === null && state.children !== null) {
            state.children.forEach(function decodeChildren(child) {
                // NOTE: We are ignoring errors here, to let parser continue with other
                // parts of encoded data
                child._decode(input, options);
            });
        }
        // Decode contained/encoded by schema, only in bit or octet strings
        if (state.contains && (state.tag === 'octstr' || state.tag === 'bitstr')) {
            const data = new DecoderBuffer(result);
            result = this._getUse(state.contains, input._reporterState.obj)
                ._decode(data, options);
        }
    }
    // Pop object
    if (state.obj && present)
        result = input.leaveObject(prevObj);
    // Set key
    if (state.key !== null && (result !== null || present === true))
        input.leaveKey(prevKey, state.key, result);
    else if (prevKey !== null)
        input.exitKey(prevKey);
    return result;
};
Node.prototype._decodeGeneric = function decodeGeneric(tag, input, options) {
    const state = this._baseState;
    if (tag === 'seq' || tag === 'set')
        return null;
    if (tag === 'seqof' || tag === 'setof')
        return this._decodeList(input, tag, state.args[0], options);
    else if (/str$/.test(tag))
        return this._decodeStr(input, tag, options);
    else if (tag === 'objid' && state.args)
        return this._decodeObjid(input, state.args[0], state.args[1], options);
    else if (tag === 'objid')
        return this._decodeObjid(input, null, null, options);
    else if (tag === 'gentime' || tag === 'utctime')
        return this._decodeTime(input, tag, options);
    else if (tag === 'null_')
        return this._decodeNull(input, options);
    else if (tag === 'bool')
        return this._decodeBool(input, options);
    else if (tag === 'objDesc')
        return this._decodeStr(input, tag, options);
    else if (tag === 'int' || tag === 'enum')
        return this._decodeInt(input, state.args && state.args[0], options);
    if (state.use !== null) {
        return this._getUse(state.use, input._reporterState.obj)
            ._decode(input, options);
    }
    else {
        return input.error('unknown tag: ' + tag);
    }
};
Node.prototype._getUse = function _getUse(entity, obj) {
    const state = this._baseState;
    // Create altered use decoder if implicit is set
    state.useDecoder = this._use(entity, obj);
    assert(state.useDecoder._baseState.parent === null);
    state.useDecoder = state.useDecoder._baseState.children[0];
    if (state.implicit !== state.useDecoder._baseState.implicit) {
        state.useDecoder = state.useDecoder.clone();
        state.useDecoder._baseState.implicit = state.implicit;
    }
    return state.useDecoder;
};
Node.prototype._decodeChoice = function decodeChoice(input, options) {
    const state = this._baseState;
    let result = null;
    let match = false;
    Object.keys(state.choice).some(function (key) {
        const save = input.save();
        const node = state.choice[key];
        try {
            const value = node._decode(input, options);
            if (input.isError(value))
                return false;
            result = { type: key, value: value };
            match = true;
        }
        catch (e) {
            input.restore(save);
            return false;
        }
        return true;
    }, this);
    if (!match)
        return input.error('Choice not matched');
    return result;
};
//
// Encoding
//
Node.prototype._createEncoderBuffer = function createEncoderBuffer(data) {
    return new EncoderBuffer(data, this.reporter);
};
Node.prototype._encode = function encode(data, reporter, parent) {
    const state = this._baseState;
    if (state['default'] !== null && state['default'] === data)
        return;
    const result = this._encodeValue(data, reporter, parent);
    if (result === undefined)
        return;
    if (this._skipDefault(result, reporter, parent))
        return;
    return result;
};
Node.prototype._encodeValue = function encode(data, reporter, parent) {
    const state = this._baseState;
    // Decode root node
    if (state.parent === null)
        return state.children[0]._encode(data, reporter || new Reporter());
    let result = null;
    // Set reporter to share it with a child class
    this.reporter = reporter;
    // Check if data is there
    if (state.optional && data === undefined) {
        if (state['default'] !== null)
            data = state['default'];
        else
            return;
    }
    // Encode children first
    let content = null;
    let primitive = false;
    if (state.any) {
        // Anything that was given is translated to buffer
        result = this._createEncoderBuffer(data);
    }
    else if (state.choice) {
        result = this._encodeChoice(data, reporter);
    }
    else if (state.contains) {
        content = this._getUse(state.contains, parent)._encode(data, reporter);
        primitive = true;
    }
    else if (state.children) {
        content = state.children.map(function (child) {
            if (child._baseState.tag === 'null_')
                return child._encode(null, reporter, data);
            if (child._baseState.key === null)
                return reporter.error('Child should have a key');
            const prevKey = reporter.enterKey(child._baseState.key);
            if (typeof data !== 'object')
                return reporter.error('Child expected, but input is not object');
            const res = child._encode(data[child._baseState.key], reporter, data);
            reporter.leaveKey(prevKey);
            return res;
        }, this).filter(function (child) {
            return child;
        });
        content = this._createEncoderBuffer(content);
    }
    else {
        if (state.tag === 'seqof' || state.tag === 'setof') {
            // TODO(indutny): this should be thrown on DSL level
            if (!(state.args && state.args.length === 1))
                return reporter.error('Too many args for : ' + state.tag);
            if (!Array.isArray(data))
                return reporter.error('seqof/setof, but data is not Array');
            const child = this.clone();
            child._baseState.implicit = null;
            content = this._createEncoderBuffer(data.map(function (item) {
                const state = this._baseState;
                return this._getUse(state.args[0], data)._encode(item, reporter);
            }, child));
        }
        else if (state.use !== null) {
            result = this._getUse(state.use, parent)._encode(data, reporter);
        }
        else {
            content = this._encodePrimitive(state.tag, data);
            primitive = true;
        }
    }
    // Encode data itself
    if (!state.any && state.choice === null) {
        const tag = state.implicit !== null ? state.implicit : state.tag;
        const cls = state.implicit === null ? 'universal' : 'context';
        if (tag === null) {
            if (state.use === null)
                reporter.error('Tag could be omitted only for .use()');
        }
        else {
            if (state.use === null)
                result = this._encodeComposite(tag, primitive, cls, content);
        }
    }
    // Wrap in explicit
    if (state.explicit !== null)
        result = this._encodeComposite(state.explicit, false, 'context', result);
    return result;
};
Node.prototype._encodeChoice = function encodeChoice(data, reporter) {
    const state = this._baseState;
    const node = state.choice[data.type];
    if (!node) {
        assert(false, data.type + ' not found in ' +
            JSON.stringify(Object.keys(state.choice)));
    }
    return node._encode(data.value, reporter);
};
Node.prototype._encodePrimitive = function encodePrimitive(tag, data) {
    const state = this._baseState;
    if (/str$/.test(tag))
        return this._encodeStr(data, tag);
    else if (tag === 'objid' && state.args)
        return this._encodeObjid(data, state.reverseArgs[0], state.args[1]);
    else if (tag === 'objid')
        return this._encodeObjid(data, null, null);
    else if (tag === 'gentime' || tag === 'utctime')
        return this._encodeTime(data, tag);
    else if (tag === 'null_')
        return this._encodeNull();
    else if (tag === 'int' || tag === 'enum')
        return this._encodeInt(data, state.args && state.reverseArgs[0]);
    else if (tag === 'bool')
        return this._encodeBool(data);
    else if (tag === 'objDesc')
        return this._encodeStr(data, tag);
    else
        throw new Error('Unsupported tag: ' + tag);
};
Node.prototype._isNumstr = function isNumstr(str) {
    return /^[0-9 ]*$/.test(str);
};
Node.prototype._isPrintstr = function isPrintstr(str) {
    return /^[A-Za-z0-9 '()+,-./:=?]*$/.test(str);
};
export default Node;
